Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better TS #302

Merged
merged 15 commits into from
Nov 8, 2024
Merged

Better TS #302

merged 15 commits into from
Nov 8, 2024

Conversation

lishaduck
Copy link
Contributor

@lishaduck lishaduck commented Nov 6, 2024

Follow up to #301.

Works on TSESlint-based safety.
Probably broke some stuff with error handling.

Please review closely,1 some of these changes might be better served with ignores. Then again, ignores are evil, so... I just fixed them all and hoped for the best.

Oh, and I added more JSDocs for literally everything! Yay!

Footnotes

  1. EDIT: 🤣

I needed better types for `parse-elm`, and ended up with this beauty.

Disclaimer: `NestedEntrypoints` type courtesy of Copilot.
You can throw anything.
Now, we only have to deal with Errors.
Probably a slight perf hit, but likely only on the unhappy path, so it's not as critical.
Uhhhh......
Effect is heavy, and TS didn't like the way message passing was done...
And, so I uh... wrote a result type.
And then I made it generic, so voilà!

The only issue is that TS can't type pipes,
so I'd need to make a fluent interface or write a ton of overloads.
I opted to just force normal functions.
Copy link

socket-security bot commented Nov 6, 2024

New and removed dependencies detected. Learn more about Socket for GitHub ↗︎

Package New capabilities Transitives Size Publisher
npm/@package-json/[email protected] None 0 17.8 kB kwaa

View full report↗︎

Address review comments from jfmengels#301.
@lishaduck lishaduck marked this pull request as ready for review November 6, 2024 18:35
@jfmengels, without more context for the intent of this code,
I'm not sure where the floating promises were intentional.
I've marked them as ignored so that we don't introduce more issues,
but I'd appreciate any insight you can provide on further steps.
@lishaduck
Copy link
Contributor Author

@jfmengels, ping pong 🏓
:)

* @param {unknown} error
*/
function intoError(error) {
return error instanceof Error ? error : new Error(String(error));
Copy link
Owner

@jfmengels jfmengels Nov 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious, did you notice places where we (potentially) wrapped an exception into multiple Errors? (nested I mean)

Related: did you notice places where we throw non-Errors? 🤔
(Note, I haven't read the rest of the commits yet)

Copy link
Contributor Author

@lishaduck lishaduck Nov 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious, did you notice places where we (potentially) wrapped an exception into multiple Errors? (nested I mean)

Nope.

Related: did you notice places where we throw non-Errors? 🤔 (Note, I haven't read the rest of the commits yet)

Yeah, wenode-elm-compiler throws strings in a few places.

const ElmCommunication = require('./elm-communication');
const loadCompiledElmApp = require('./load-compiled-app');
const ResultCache = require('./result-cache');

/**
* @type {WorkerThreads<WorkerData>}
*/
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the type annotation not ideally be placed at the definition of workerThreads? (That at least sounds a lot more intuitive to me)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried that, but it doesn't like it.

@@ -15,9 +16,13 @@ if (parentPort) {
parentPort.on('message', async (url) => {
try {
const response = await getBody(url);
requestPort.postMessage(response);
requestPort.postMessage(
/** @satisfies {PortResponse} */ (result.succeed(response))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make it look even more like Elm!

const Result = require('./result');

Result.succeed(response)

Maybe even call the constructors Ok and Err (or Fail, because I can see it could be confusing with Error/exceptions)

if (flag.alias !== undefined) {
object[flag.name] = flag.alias;
}
const args = /** @type {ParsedArgs} */ (
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the difference between

const args = /** @type {ParsedArgs} */ ...

and

/** @type {ParsedArgs} */
const args = ...

Are the two different from TS' point of view?
(Or is this just a styling change?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first one is a cast, the second one is a type annotation. I don't recall why this needed a cast.

@@ -338,7 +340,10 @@ async function makeGitHubApiRequest(options, url, handleNotFound) {
// TODO(@lishaduck) [engine:node@>=21]: We can use `fetch` now.
const response = await got(url, parameters);

return response.body;
/** @type {{body: unknown}} */
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing this over and over, I feel like TS should try to understand type annotations on a return statement like

/** @type {{body: unknown}} */
return response.body;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's actually a ts-eslint issue: typescript-eslint/typescript-eslint#1682. It can't read JSDoc casts b/c they're technically just comments, so the type queries are wrong. no-unsafe-return bans returning any, which is good, but can't read immediate casts, it needs an intermediate variable. In normal TS, you can just use as.

);

return /** @type {ApplicationDependencies} */ (dependencies);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just so I understand, the /** @type {unknown} */ is for correctly annotating the value of dependencies, and the type annotation here is to cast?

Or are they both for casting? In which case, why not cast only once?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When using JSON.parse (& other lib functions), you generally want to cast to unknown, then to another type (the as unknown as pattern), because they don't default to unknown for back compat.
I'd guess it's not a direct return b/c of no-unsafe-returns.

@@ -29,7 +30,8 @@ async function collect(options, elmJson, elmVersion) {
let hasDependenciesThatCouldNotBeDownloaded = false;

const projectDepsPromises = Object.entries(dependenciesEntries).map(
async ([name, constraint]) => {
async ([pkgname, constraint]) => {
const name = /** @type {PackageName} */ (pkgname);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Mostly out of curiosity) Could this type potentially be inferred by typing dependenciesEntries instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is typed, but because of excess properties, ts can't infer it: FAQ § Excess Properties Are OK.

@@ -12,15 +12,16 @@ expect.extend({toMatchFile});

/**
* @param {string} args
* @param {Options} [options=undefined]
* @param {Options} [options]
* @returns
Copy link
Owner

@jfmengels jfmengels Nov 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the type not missing here? (same in the rest of the file)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With options, the [] syntax defaults to undefined.
With the @returns, I must've just missed it.

@@ -139,7 +139,7 @@ async function initializeApp(options, elmModulePath, reviewElmJson, appHash) {
* @returns {never}
*/
(errors) => {
Report.report(options, {errors});
void Report.report(options, {errors});
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I knew that worker.terminate() returned a Promise back when I wrote it, but I think all the floating promises in this commit make sense to keep as floating (for concurrency purposes, etc.).

The only one I'd be worried about it potentially this one, but so far it has always worked well so... I think it's fine to keep as is 🤷

@jfmengels jfmengels merged commit a991e03 into jfmengels:main Nov 8, 2024
3 checks passed
@jfmengels
Copy link
Owner

Great work!

Btw, thank you for your nice commit etiquette for your PRs. It's easy to review each commit (except maybe for 3965c34 😛 , although it was not that bad in practice)

@lishaduck lishaduck deleted the better-ts branch November 8, 2024 23:04
@lishaduck lishaduck mentioned this pull request Nov 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants